Result を生成する関数の連鎖(bind と errorMap、map)
LT;DR
Result を返す関数をスイッチ関数と呼ぶ
https://scrapbox.io/files/66af628d724792001c02f4f6.png
複数のスイッチ関数を合成すると、以下のようになるのが理想である
https://scrapbox.io/files/66af637d228b71001de21d04.png
https://scrapbox.io/files/66af64cd2ce788001de4b9e4.png
https://scrapbox.io/files/66af6d6d8875c6001c3cce1b.png
単一のトラック関数を 2 トラックの関数に変換する関数(map)もあると便利 https://scrapbox.io/files/66af67232e1e4d001c960451.png
hr.icon
アダプタブロックが必要な理由
Result を返す関数のイメージ
https://scrapbox.io/files/66af628d724792001c02f4f6.png
本書ではこれを、スイッチ関数と呼ぶ
2 つのスイッチ関数を合成するケースを考える
1 つめの関数が成功した場合: 連続する次の関数へ進む
1 つめの関数が失敗した場合: バイパスする
https://scrapbox.io/files/66af632b37c959001c73b570.png
これを踏まえると、以下のように繋げられる
https://scrapbox.io/files/66af62ed38fbed001d0efdd4.png
すべてのステップをつなぐと、以下のようになる
https://scrapbox.io/files/66af637d228b71001de21d04.png
問題点
Result を生成する関数は、合成して 1 つにできない
2 トラックモデルの出力の型は、1 トラックの入力の型と同じでないため
https://scrapbox.io/files/66af644bea0836001cadcd13.png
2 つ目の関数が 2 トラックの入力を持っていたら接続できる
https://scrapbox.io/files/66af648668c602001ccf8578.png
これを実現するためには、入力が 1 つで出力が 2 つのスイッチ関数を 2 トラックの関数に変換する 必要がある
https://scrapbox.io/files/66af64cd2ce788001de4b9e4.png
そこで、スイッチ関数を受け取り、それを 2 トラック関数に変換する アダプタブロック を作成する アダプタブロックを用いて、すべてのステップを 2 トラックの関数に変換する
https://scrapbox.io/files/66af654f5eb576001d417389.png
アダプタブロックの実装
スイッチ関数を 2 トラック関数に変換するアダプタブロック
(>>=) :: Monad m => m a -> (a -> m b) -> m b
実装
code:fsharp
// val bind: f: ('a -> Result<'b,'c>) -> aResult: Result<'a,'c> -> Result<'b,'c>
let bind f aResult =
match aResult with
| Ok success -> f success
| Error failure -> Error failure
単一トラックの関数を 2 トラックの関数に変換するアダプタブロックもあると便利である
こちらも FP では非常に重要で、map と呼ばれる code:haskell
fmap :: Functor f => (a -> b) -> f a -> f b
(<$>) :: Functor f => (a -> b) -> f a -> f b
https://scrapbox.io/files/66af67232e1e4d001c960451.png
実装
code:fsharp
// val map: f: ('a -> 'b) -> aResult: Result<'a,'c> -> Result<'b,'c>
let map f aResult =
match aResult with
| Ok success -> Ok (f success)
| Error failure -> Error failure
これらの関数は ドメイン 内のあらゆる場所で利用されるため、Result.fs などのユーティリティモジュールを作成して、そこに配置する code:fsharp
module Result =
let bind f aResult = ...
let map f aResult
合成 と 型チェック
当たり前だが、あるステップの出力型が次のステップの入力型が一致しないと合成できない
以下の例だと、FunctionA は FunctionB と合成できるが、FunctionC とは合成できない
https://scrapbox.io/files/66af6a20048b56001d3997c0.png
https://scrapbox.io/files/66af6a0942b96a001cd11079.png
code:fsharp
type FunctionA = Apple -> Result<Bananas, ...>
type FunctionB = Bananas -> Result<Cherries, ...>
type FunctionC = Cherries -> Result<Lemon, ...>
bind は以下のように利用できる
code:fsharp
let functionA: FunctionA = ...
let functionB: FunctionB = ...
let functionC: FunctionC = ...
let functionABC input =
input
|> functionA
|> Result.bind functionB
|> Result.bind functionC
共通のエラー型に変換する
現状のエラートラックでは、すべての関数が同じエラー型を持つ必要がある
エラーの型は各ステップで異なることが多いので、それぞれのエラー型に互換性を持たせる必要がある
code:fsharp
// val mapError: f: ('a -> 'b) -> aResult: Result<'c,'a> -> Result<'c,'b>
let mapError f aResult =
match aResult with
| Ok success -> Ok success
| Error failure -> Error (f failure)
e.g.
FunctionA と FunctionB はエラー型が異なるので、合成できない
code:fsharp
type FunctionA = Apple -> Result<Bananas, AppleError>
type FunctionB = Bananas -> Result<Cherries, BananasError>
そこで、AppleError と BananasError の両方が変換できる新しい 選択型 を作成する code:fsharp
type FruitError =
| AppleErrorCase of AppleError
| BananasErrorCase of BananasError
そして、functionA の Result 型が FruitError になるように変換する
code:fsharp
let functionA: FunctionA = ...
let functionAWithFruitError input =
input |> functionA |> Result.mapError AppleErrorCase
イメージ
https://scrapbox.io/files/66af6d6d8875c6001c3cce1b.png
同様に functionB も変換できる
code:fsharp
let functionB: FunctionB = ...
let functionBWithFruitError input =
input |> functionB |> Result.mapError BananasErrorCase
これにより、bind で合成が可能になる
code:fsharp
// val functionAB : Apple -> Result<Cherries, FruitError>
let functionAB input =
input
|> functionAWithFruitError
|> Result.bind functionBWithFruitError